<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Alpha‑Factory · Trace‑graph</title>
<style>
html,body{margin:0;height:100%;background:#111;color:#eee;font:14px/1.4 sans-serif}
#graph{width:100%;height:100%}
.node circle{stroke:#fff;stroke-width:1.5px}
.link{stroke:#888;stroke-opacity:.7}
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
</head>
<body>
<div id="graph"></div>

<script>
/* ------------------- d3 force‑directed graph boiler‑plate ------------- */
const width  = window.innerWidth,
      height = window.innerHeight;

const svg   = d3.select("#graph").append("svg")
               .attr("viewBox", [0, 0, width, height]);

const linkG = svg.append("g").attr("class", "links");
const nodeG = svg.append("g").attr("class", "nodes");

const sim = d3.forceSimulation()
              .force("link", d3.forceLink().distance(90).id(d => d.id))
              .force("charge", d3.forceManyBody().strength(-300))
              .force("center", d3.forceCenter(width/2, height/2));

let graph = {nodes: [], links: []};

/* --------------------------- rendering -------------------------------- */
function update() {
  const links = linkG.selectAll("line").data(graph.links, d => d.id);
  links.enter().append("line")
        .attr("class","link")
        .attr("stroke-width",2)
      .merge(links);
  links.exit().remove();

  const nodes = nodeG.selectAll("g").data(graph.nodes, d => d.id);
  const enter = nodes.enter().append("g").call(d3.drag()
                   .on("start", dragstarted)
                   .on("drag", dragged)
                   .on("end", dragended));

  enter.append("circle").attr("r", 10).attr("fill", "#1f77b4");
  enter.append("title").text(d => d.label);

  nodes.exit().remove();

  sim.nodes(graph.nodes).on("tick", () => {
    linkG.selectAll("line")
         .attr("x1", d => d.source.x)
         .attr("y1", d => d.source.y)
         .attr("x2", d => d.target.x)
         .attr("y2", d => d.target.y);

    nodeG.selectAll("g")
         .attr("transform", d => `translate(${d.x},${d.y})`);
  });

  sim.force("link").links(graph.links);
  sim.alpha(1).restart();
}

function dragstarted(event) {
  if (!event.active) sim.alphaTarget(0.3).restart();
  event.subject.fx = event.subject.x;
  event.subject.fy = event.subject.y;
}
function dragged(event) {
  event.subject.fx = event.x;
  event.subject.fy = event.y;
}
function dragended(event) {
  if (!event.active) sim.alphaTarget(0);
  event.subject.fx = event.subject.fy = null;
}

/* ---------------------------- WebSocket ------------------------------- */
const ws = new WebSocket(`${location.protocol.replace("http","ws")}//${location.host}/ws/trace`);

ws.onmessage = e => {
  try {
    const msg = JSON.parse(e.data);           // {id,label,edges:[targetId,...]}
    if (!graph.nodes.find(n => n.id === msg.id)) {
      graph.nodes.push({id: msg.id, label: msg.label});
    }
    msg.edges?.forEach(t => {
      const id = `${msg.id}->${t}`;
      if (!graph.links.find(l => l.id === id)) {
        graph.links.push({id, source: msg.id, target: t});
      }
    });
    update();
  } catch(err) {
    console.error(err);
  }
};

ws.onopen    = () => console.log("Trace‑WS connected");
ws.onerror   = err => console.error("Trace‑WS error:", err);
ws.onclose   = () => console.warn("Trace‑WS closed");
</script>
</body>
</html>

